package org.ndx.jsg;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.ConfigurationUtils;
import org.apache.commons.configuration.XMLConfiguration;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.Option;
import org.ndx.jsg.config.DefaultConfiguration;
import org.ndx.jsg.input.FileInfosProvider;
import org.ndx.jsg.output.OutputManager;

import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;

/**
 * Hello world!
 * 
 */
public class App {
	/**
	 * Default name of config file
	 */
	private static final String CONFIG_JSG_XML = "config.jsg.xml";
	private static final Logger logger = Logger.getLogger(App.class.getName());
	
	/**
	 * The classical main method
	 * @param args
	 * @category main
	 */
	public static void main(String[] args) {
		new App().run(args);
	}

	/**
	 * Used configuration file
	 */
	@Option(name="-c", aliases= {"--configFile"}, usage="gives the path of the used config file. In case this file do not exists, it is created with default values")
	private File configFile = new File(CONFIG_JSG_XML);

	/**
	 * Create and save default configuration file in path indicated by {@link #configFile}
	 * @return {@link DefaultConfiguration}
	 * @category config
	 */
	private Configuration createDefaultConfiguration() {
		Configuration returned;
		returned = DefaultConfiguration.create();
		if(!configFile.exists()) {
			File absolute = new File(configFile.getAbsolutePath());
			if(!absolute.getParentFile().exists()) {
				absolute.getParentFile().mkdirs();
			}
			try {
				FileWriter writer = new FileWriter(absolute);
				XMLConfiguration xml = new XMLConfiguration();
				ConfigurationUtils.copy(returned, xml);
				xml.save(writer);
				writer.close();
			} catch(Exception e) {
				logger.log(Level.SEVERE, "unable to backup used configuration to file "+configFile.getAbsolutePath(), e);
			}
		}
		return returned;
	}

	/**
	 * Generates the web site
	 * @category generate
	 */
	private void generate() {
		Configuration config = loadConfig();
		Injector injector = Guice.createInjector(getModules(Arrays.asList(config.getStringArray(DefaultConfiguration.MODULES_LIST))));
		Collection<FileInfos> infos = getInputFileInfos(config, injector);
		logger.info("found a total of "+infos.size()+" files to process");
		createOutputFiles(config, infos, injector);
	}

	/**
	 * From user config, input files and guice injector, creates the set of output file using whichever required mechanism
	 * @param config
	 * @param infos
	 * @param injector
	 */
	private void createOutputFiles(Configuration config,
			Collection<FileInfos> infos, Injector injector) {
		Set<OutputManager> outputters = injector.getInstance(Key.get(new TypeLiteral<Set<OutputManager>>() {}));
		/* 
		 * Before to output anything, we correct the provided output if required
		 */
		File output = new File(config.getString(DefaultConfiguration.OUTPUT_PATH));
		if(!output.exists()) {
			logger.info("your output path ("+output.getPath()+") does not seems to exists, we will suppose it's a children directory of "+configFile.getParent());
			File correctedOutput = new File(configFile.getParentFile()+File.separator+output.getPath());
			try {
				config.setProperty(DefaultConfiguration.OUTPUT_PATH, correctedOutput.getCanonicalPath());
			} catch (IOException e) {
				config.setProperty(DefaultConfiguration.OUTPUT_PATH, correctedOutput.getAbsolutePath());
			}
			if(!correctedOutput.exists()) {
				correctedOutput.mkdirs();
			}
		}
		for(OutputManager manager : outputters) {
			manager.output(config, infos);
		}
	}

	/**
	 * Load file infos using various methods
	 * @param config used configuration
	 * @param injector guice injector, used to get plugins for loading files
	 * @return a collection of file infos to load
	 * @category input
	 */
	private Collection<FileInfos> getInputFileInfos(Configuration config, Injector injector) {
		return injector.getInstance(FileInfosProvider.class).getFiles(
				configFile.getParentFile(),
				config.getStringArray(DefaultConfiguration.INPUT_PATHS), 
				Arrays.asList(config.getStringArray(DefaultConfiguration.INPUT_IGNORED_NAMES)));
	}

	/**
	 * Produces a list of modulesfrom a list of class names
	 * @param modulesNames an iterable of modules class names to be instanciated using empty constructor
	 * @return an iterable of modules usable by Guice
	 * @category config
	 */
	private Iterable<Module> getModules(Iterable<String> modulesNames) {
		Collection<Module> returned = new LinkedList<Module>();
		for(String name : modulesNames) {
			try {
				Object instance = Class.forName(name).newInstance();
				if(instance instanceof Module) {
					returned.add((Module) instance);
				} else {
					logger.log(Level.WARNING, "unable to use object "+name+" as module, since it is not one !");
				}
			} catch (Exception e) {
				logger.log(Level.WARNING, "unable to use object "+name+" as module, since it cannot be instanciated using no-args constructor", e);
			}
		}
		return returned;
	}

	/**
	 * Load config, or generate a default one
	 * @return a {@link Configuration} object containing most of default config. Optionnal config elements are not handled here.
	 * @category config
	 */
	private Configuration loadConfig() {
		Configuration returned = null;
		if(configFile.exists()) {
			logger.fine("loading config file from "+configFile.getAbsolutePath());
			if(configFile.isDirectory()) {
				configFile = new File(configFile.getAbsolutePath()+File.separator+CONFIG_JSG_XML);
				logger.fine("but it's a directory ! Don't be ashamed, we will use file with default name :"+configFile.getAbsolutePath());
			}
			if(configFile.exists()) {
				try {
					returned = new XMLConfiguration(configFile);
				} catch (ConfigurationException e) {
					logger.log(Level.WARNING, "unable to load configuration (%s), replacing it with default", e.getMessage());
				}
			}
		}
		if(returned==null) {
			returned = createDefaultConfiguration();
		}
		// Config file path is put in configuration for later reference
		returned.addProperty(DefaultConfiguration.CONFIGURATION_FILE, configFile.getAbsolutePath());
		return returned;
	}

	/**
	 * Effectively runs the app by parsing the given arguments
	 * @param args input arguments
	 * @category main
	 */
	private void run(String[] args) {
		CmdLineParser parser = new CmdLineParser(this);
		try {
			parser.parseArgument(args);
			generate();
		} catch (CmdLineException e) {
			e.printStackTrace();
			parser.printUsage(System.err);
		}
	}
}
